﻿namespace VirtualTemplates
{
    using System;
    using System.Diagnostics.Contracts;
    using System.Reflection;
    using System.Web;
    using System.Web.Hosting;

    /// <summary>
    /// Makes asp.net handling files that are usually not handled by asp.net and enables client caching for them.
    /// </summary>
    internal class VirtualFileHandler : IHttpHandler
    {
        /// <summary>
        /// Uses the internal static file handler class to process the request.
        /// </summary>
        private static readonly Action<HttpContext> ProcessRequestInternal;

        private readonly VirtualFileCachePolicy cache = new VirtualFileCachePolicy();

        /// <summary>
        /// Initializes static members of the <see cref="VirtualFileHandler" /> class.
        /// </summary>
        static VirtualFileHandler()
        {
            var type = typeof(HttpContext).Assembly.GetType("System.Web.StaticFileHandler");
            var handler = type.GetConstructor(BindingFlags.NonPublic | BindingFlags.Instance, null, Type.EmptyTypes, null).Invoke(null);

            ProcessRequestInternal = httpContext => type.GetMethod("ProcessRequest").Invoke(handler, new object[] { httpContext });
        }

        /// <summary>
        /// Gets the http cache policy for virtual files.
        /// </summary>
        public VirtualFileCachePolicy Cache
        {
            get { return this.cache; }
        }

        /// <summary>
        /// Gets a value indicating whether another request can use the <see cref="T:System.Web.IHttpHandler"/> instance.
        /// </summary>
        /// <returns>true if the <see cref="T:System.Web.IHttpHandler"/> instance is reusable; otherwise, false.</returns>
        public bool IsReusable
        {
            get { return true; }
        }

        /// <summary>
        /// Processes HTTP Web requests on virtual files and adds client caching information to the headers.
        /// </summary>
        /// <param name="context">An <see cref="T:System.Web.HttpContext"/> object that provides references to the intrinsic server objects (for example, Request, Response, Session, and Server) used to service HTTP requests.</param>
        public void ProcessRequest(HttpContext context)
        {
            var path = context.Request.CurrentExecutionFilePath;

            var cacheDependency = HostingEnvironment.VirtualPathProvider.GetCacheDependency(path, new[] { path }, DateTime.UtcNow);
            if (cacheDependency == null)
            {
                ProcessRequestInternal(context);
                return;
            }

            var response = context.Response;

            var lastModified = cacheDependency.UtcLastModified.ToLocalTime();

            if (Modified(context.Request, lastModified))
            {
                response.Cache.SetMaxAge(this.Cache.MaxAge);
                response.Cache.SetLastModified(lastModified);
                ProcessRequestInternal(context);
            }
            else
            {
                response.StatusCode = 304;
                response.StatusDescription = "Not Modified";
                response.AddHeader("Content-Length", "0");
            }
        }

        /// <summary>
        /// Checks if a resource has been modified for a specified HTTP request.
        /// </summary>
        /// <param name="httpRequest">The HTTP request.</param>
        /// <param name="lastModified">The last modified date.</param>
        /// <returns>true if the resource has been modified, otherwise false.</returns>
        private static bool Modified(HttpRequest httpRequest, DateTime lastModified)
        {
            Contract.Requires(httpRequest != null);

            DateTime modifiedSince;

            var header = httpRequest.Headers["If-Modified-Since"];

            return header == null || !DateTime.TryParse(header, out modifiedSince) || modifiedSince < ToShortDate(lastModified);
        }

        /// <summary>
        /// Converts a date time to a short date time which is punctual to the second.
        /// </summary>
        /// <param name="dateTime">The date time.</param>
        /// <returns>The short date time.</returns>
        private static DateTime ToShortDate(DateTime dateTime)
        {
            return new DateTime(dateTime.Year, dateTime.Month, dateTime.Day, dateTime.Hour, dateTime.Minute, dateTime.Second);
        }
    }
}
